﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace FunctionalProgramming.PartialApplication
{
   /// <summary>
   /// Encapsulates process of partial application of function.
   /// </summary>
   public static class PartialApplicator
   {
      /// <summary>
      /// Applies the specified function partially.
      /// </summary>
      /// <param name="function">The function.</param>
      /// <param name="arguments">The arguments for function.</param>
      /// <returns>If all arguments for function are provided, the function is invoked and result of this invocation is returned. If there
      /// are still some arguments missing, the function that expects missing arguments is returned instead.</returns>
      public static dynamic Apply(Delegate function, params object[] arguments)
      {
         ParameterInfo[] functionParameters = GetParametersOfFunction(function);

         ThrowIfArgumentsInvalid(arguments, functionParameters);

         ParameterExpression[] missingArguments = GetExpressionsForMissingArguments(arguments, functionParameters);
         ConstantExpression[] providedArguments = GetExpressionsForProvidedArguments(arguments);

         Delegate partiallyAppliedFunction = ApplyFunctionPartially(function, missingArguments, providedArguments);

         dynamic result = GetResultOfApplication(partiallyAppliedFunction, missingArguments);

         return result;
      }

      private static ParameterInfo[] GetParametersOfFunction(Delegate function)
      {
         return function.Method.GetParameters()
            // parameter of type closure is added automatically when compiling the expression
            // its value will be determined by CLR, so it is of no interest to us
            .Where(x => x.ParameterType != typeof(Closure))
            .ToArray();
      }

      private static void ThrowIfArgumentsInvalid(object[] arguments, ParameterInfo[] functionParameters)
      {
         if (arguments.Length == 0 || functionParameters.Length < arguments.Length)
         {
            throw new InvalidOperationException();
         }
      }

      private static ParameterExpression[] GetExpressionsForMissingArguments(object[] arguments, ParameterInfo[] functionParameters)
      {
         return functionParameters
            .Skip(arguments.Length)
            .Select(x => Expression.Parameter(x.ParameterType))
            .ToArray();
      }

      private static ConstantExpression[] GetExpressionsForProvidedArguments(object[] arguments)
      {
         return arguments.Select(Expression.Constant).ToArray();
      }

      private static Delegate ApplyFunctionPartially(Delegate function, ParameterExpression[] missingArguments, Expression[] providedArguments)
      {
         Expression body = Expression.Call(
            Expression.Constant(function), 
            function.GetType().GetMethod("Invoke"),
            providedArguments.Concat(missingArguments));

         Delegate partiallyAppliedFunction = Expression.Lambda(body, missingArguments).Compile();
         return partiallyAppliedFunction;
      }

      private static dynamic GetResultOfApplication(Delegate partiallyAppliedFunction, ParameterExpression[] missingArguments)
      {
         dynamic result;
         if (missingArguments.Length == 0)
         {
            // with no missing arguments, we can get invoke the function immediately and get the results
            result = partiallyAppliedFunction.DynamicInvoke();
         }
         else
         {
            result = partiallyAppliedFunction;
         }
         return result;
      }
   }
}